"""Classes to implement the network pane of the Dingo GUI.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
"""

__author__ = 'John Perks, Sarah Mount'
__credits__ = "Based on OGL.py by Jeff Grimmett (grimmtooth@softhome.net) from the wxPython distribution."

from Dingo.pubsub import Publisher

import wx, wx.lib.ogl as ogl
import consts, sched, sys, os, threading, types, time, itertools
import Dingo.locks as locks

import pdb, gc

class wxThreadSwitch(object):
    """Decorator class to ensure wxPython calls are executed in correct thread"""
    def __init__(self, callable):
        object.__init__(self)
        self.callable = callable
    
    def __get__(self, inst, owner=None):
        """Forwarding of descriptor magic (function -> method etc)"""
        c = self.callable
        # if c is a descriptor then wrap it around
        # the instance as would have happened normally
        if not isinstance(c, types.InstanceType):
            get = None
            try:
                get = c.__get__
            except AttributeError:
                pass
            if get is not None:
                return wxThreadSwitch(get(inst, owner))
        # if we get here, then not a descriptor,
        # so return self unchanged
        return self

    def __call__(self, *args, **kwargs):
        """Switch threads if needed"""
        if wx.Thread_IsMain():
            return self.callable(*args, **kwargs)
        
        locks.Check()
        c = self.__wxThreadCall(self.callable)
        wx.CallAfter(c, *args, **kwargs)
        return c.Result()

    class __wxThreadCall(object):
        """The actual thread-switched call itself"""
        def __init__(self, callable):
            assert not wx.Thread_IsMain()
            object.__init__(self)
            self.callable = callable
            self.result = None
            self.exc_info = None
            self.event = threading.Event()

        def __call__(self, *args, **kwargs):
            try:
                try:
                    assert wx.Thread_IsMain()
                    assert not self.event.isSet()
                    self.result = self.callable(*args, **kwargs)
                except:
                    self.exc_info = sys.exc_info()
            finally:
                self.event.set()

        def Result(self):
            self.event.wait()
            if self.exc_info:
                type, value, traceback = self.exc_info
                raise type, value, traceback
            return self.result



class DeletedException(Exception):
    """Raised when a deleted SensorAvatar or MsgAvatar has a method called after deletion.

    Instructions to raise this exception have mostly been
    deleted. Cancelling a simulation often results in method calls to
    Avatars that no longer exist on the canvas and for large
    simulations it is time consuming to close all the dialog windows
    that result from this exception being raised.
    """
    pass


@wxThreadSwitch
def PollNetwork():
    """The thread-switching code that wraps this function checks if the
    Simulation  is still running and is not cancelled. So this function itself
    does nothing."""
    pass

SleepPollTime = 0.1
"""var: Time in seconds to sleep when polling network. See L{Dingo.OGL.Sleep}.
@type: float
"""

def Sleep(t):
    """Pauses the current thread, but periodically polls the network in
    case of cancellation.

    @type t: float
    @param t: Time in seconds to sleep.
    """
    start = time.time()
    while time.time() - start < t:
        PollNetwork()
        time.sleep(SleepPollTime)

class SensorGraph(ogl.ShapeCanvas):
    """The Graph on which Avatars and Arcs are placed.
    The sensor graph is a visual representation of the simulated
    network.  Each sensor node is displayed on the graph allowing the
    user to gain insight into what is happening within the network,
    The displayed sensors can be moved around the graph panel
    affecting there simulated locations.  Communications between nodes
    are displayed on the graph as arcs between the communicating
    nodes.
    """
    def __init__(self, parent, app):
        """Initialise the SensorGraph.
        Helper method, called by the UI in L{Dingo.Main.DingoApp}
        """
        self.bgImage = False
        ogl.OGLInitialize()
        SensorColor._setupColours()
        SensorGraph.Instance = self
        ogl.ShapeCanvas.__init__(self, parent)
        self._app = app
        self.SetClientSize(parent.GetSize()) # Apparently more portable than GetSize()
        self.EnableScrolling(True, True)
        self.SetScrollbars(20, 20, self.GetMaxWidth()/20, self.GetMaxHeight()/20) # BROKEN
        self.SetBackgroundColour(wx.NamedColour('Dark Olive Green'))
        self.SetScale(1.0, 1.0)
        self.ClearGraph()
        self.Bind(wx.EVT_SIZE, self.OnResize)

	self.avatars = {}
	"""@ivar: dictionary of L{Dingo.OGL.SensorAvatar} objects, keyed on id of represented sensor."""
        
	# Subscribe to topics regarding radio messages
	Publisher().subscribe(self.OnDrawMsg,    (consts.RADIO_TOPIC))
	# Subscribe to topics regarding sensors        
        Publisher().subscribe(self.OnNewAvatar,  (consts.NEW_AVATAR_TOPIC))
        Publisher().subscribe(self.OnSetColour,  (consts.COLOUR_TOPIC))
        Publisher().subscribe(self.OnSetShape,   (consts.SHAPE_TOPIC))
        Publisher().subscribe(self.OnMoveAvatar, (consts.MOVE_TOPIC))
	Publisher().subscribe(self.OnDetachGUI,  (consts.DETACH_TOPIC))
        return

    def OnDetachGUI(self, msg):
	"""This method must NOT be thread swtiched (or pubsub will barf).
	"""
        self.ClearGraph()
	return

    def OnNewAvatar(self, msg):
        """Creates a new SensorAvatar object and places it on the canvas.
        Triggered by a topic subscription.
        """
        if msg.data['id'] in self.avatars: return
        self.AddSensorAvatar(msg.data['id'], msg.data['coords'], msg.data['colour'], msg.data['size'], msg.data['shape'])
        return

    def OnMoveAvatar(self, msg):
        """Moves a given avatar on the canvas.
        Triggered by a topic subscription.
        """
        self.avatars[msg.data['id']].MoveAvatar(msg.data['coords'])
        return

    def OnSetColour(self, msg):
        """Set the colour of an individual avatar.
        Triggered by a topic subscription.        
        """
        self.avatars[msg.data['id']].SetAvatarColour(msg.data['colour'])
        return

    def OnSetShape(self, msg):
        """Set the shape of an individual avatar.
        Triggered by a topic subscription.
        """
        self.avatars[msg.data['id']].SetAvatarShape(msg.data['shape'])
        return

    def OnDrawMsg(self, msg):
        """Event handler for DrawMsg events. 
        Triggered by a topic subscription.
        """
        self.drawMsg(msg.data['sender'], msg.data['receivers'], bcast=msg.data['bcast'],
                     colour=msg.data['colour'], isdropped=msg.data['isdropped'])
        return

    # Should not run in GUI thread. Don't use wxThreadSwitch here.
    def drawMsg(self, sender=None, receivers=None, bcast=False, colour=None, isdropped=False):
        """Visualise a radio message.

        @precondition: len(receivers) > 0

        @type sender: SensorAvatar
        @param sender: id of the message sender
        @type receivers: list
        @param receivers: ids of the set of nodes to receive the message
        @type bcast: bool
        @param bcast: whether or not the message is a broadcast
        @type colour: SensorColour
        @param colour: colour of the message visualisation
        """
        if not receivers: return # Just in case!        
        if colour is None: colour = SensorColour.Grey
        sleeptime = 0.75
        arcs = []
        pen = None

        if bcast:
            self.avatars[sender].SetPen(None)
            self.avatars[sender].SetPen(BcastAvatarPen())

	for dest in receivers:
            if self.avatars[sender].GetArcFrom(self.avatars[dest]) is None:
                arc = MsgAvatar(self.avatars[sender], self.avatars[dest], colour, isdashed=isdropped)
		arcs.append(arc)

        Sleep(sleeptime)

        for arc in arcs:
            arc.DeleteFromGraph()
            
        if bcast:
            self.avatars[sender].SetPen(None)
            self.avatars[sender].SetPen(AvatarPen())

        self.Refresh()

        return

    @wxThreadSwitch
    def OnResize(self, event):
        self.SetClientSize(self.GetParent().GetSize())
        return

    @wxThreadSwitch
    def AddSensorAvatar(self, id, xy, colour, size=10, shape=None):
        """Creates a new Avatar, and adds it to the Graph
        Creates a new node in the graph, The user can specify the
        coordinates, colour and size.  The size of the created node is
        set to 10 as default but in simulations with many nodes the
        user may wish to decrease this producing a less cluttered
        graph

        @type xy: tuple
        @param xy:  The Coordinates of the node in a Tuple of form (X,Y)
        @type colour: Dingo.OGL.SensorColour
        @param colour: The colour of the created node
        @type size: number
        @param size: The diameter of the generated node on the display.
        @type shape: str or list
        @param shape: Default shape of the new network node. Can be one of C{"circle"}, C{"square"}, C{"triangle"} or a list of points.
        """
        if id in self.avatars: return
        
        if not shape:
            self.avatars[id] = CircleAvatar(id, self, xy, colour, size)
        elif shape == "circle":
            self.avatars[id] = CircleAvatar(id, self, xy, colour, size)
        elif shape == "square":
            self.avatars[id] =  SquareAvatar(id, self, xy, colour, size)
        elif shape == "triangle":
            self.avatars[id] =  TriangleAvatar(id, self, xy, colour, size)
        elif isinstance(shape, list):
            self.avatars[id] = PolyAvatar(id, self, xy, colour, size)
        else:
            self.avatars[id] =  CircleAvatar(id, self, xy, colour, size)
        return 

    @wxThreadSwitch
    def GetAvatars(self):
        """API: Returns a list of all Avatars on the Graph"""
        return self.avatars.values()

    @wxThreadSwitch
    def GetArcs(self):
        """API: Returns a list of all Arcs on the Graph"""
        return [s for s in self.GetDiagram().GetShapeList() if isinstance(s, MsgAvatar)]

    @wxThreadSwitch
    def ClearGraph(self):
        """API: Clears all Avatars and Arcs off the Graph"""
        d = self.GetDiagram()
        if d:
            #for node in self.GetAvatars():
            #    node.DeleteFromGraph()
            for avatar in self.avatars:
		self.avatars[avatar].DeleteFromGraph()
            del self.avatars
            self.avatars = {}
            dc = wx.ClientDC(self)
            self.PrepareDC(dc)
            d.Clear(dc)
            d.SetCanvas(None)
        d = ogl.Diagram()
        d.SetCanvas(self)
        self.SetDiagram(d)
        self.Refresh()
        if self.bgImage != False:
            self.addBackground(self.bgImage)
        ogl.OGLCleanUp()        

    @wxThreadSwitch
    def addBackground(self, theImage=None):
        """Add a background image to the graph.
        This allows the user to add a background image to the graph,
        useful if the sensors need to be positioned according to a
        map.  In simulations involving graphical data maps these can
        be loaded into the graph, the sensors are then able to read
        the value at this location of the image.  This can be useful
        when dealing with pre generated height maps etc.

        @type theImage: str
        @param theImage: Path to an image to load.
        """
        #Sortout the Path
        cwd = os.getcwd() # Bad idea really.
        self.bgImage = cwd + theImage
        #Get the Canvas and shape list
        d = self.GetDiagram()
        cvs = d.GetCanvas()
        #Load the Image
        img = wx.Bitmap(self.bgImage)
        #Create the bitmap Shape
        theShape2 = ogl.BitmapShape()
        theShape2.SetBitmap(img)
        #Insert It
        cvs.InsertShape(theShape2)
        theShape2.Show(True)
        w = theShape2.GetWidth()
        h = theShape2.GetHeight()
        #Stop it from being dragable
        theShape2.SetDraggable(False,False)
        dc = wx.ClientDC(cvs)
        #Move the image so it is aligned Correctly
        theShape2.Move(dc,(w/2),(h/2),True)
        cvs.Refresh()


class SensorAvatar(object):
    def __init__(self, id=None):
        self.id = id
        return

    @wxThreadSwitch
    def SetAvatarShape(self, shape):
        if not shape:
            self.__class__ = CircleAvatar
            CircleAvatar.__init__(self, self.id, self.GetCanvas(), (self.GetX(), self.GetY()), SensorColour(self.GetBrush().GetColour()))
        if shape == "circle":
            self.__class__ = CircleAvatar
            CircleAvatar.__init__(self, self.id, self.GetCanvas(), (self.GetX(), self.GetY()), SensorColour(self.GetBrush().GetColour()))
        elif shape == "square":
            self.__class__ = SquareAvatar
            SquareAvatar.__init__(self, self.id, self.GetCanvas(), (self.GetX(), self.GetY()), SensorColour(self.GetBrush().GetColour()))
        elif shape == "triangle":
            self.__class__ = TriangleAvatar
            TriangleAvatar.__init__(self, self.id, self.GetCanvas(), (self.GetX(), self.GetY()), SensorColour(self.GetBrush().GetColour()))
        elif type(shape) == list:
            self.__class__ = PolyAvatar
        else:
            self.__class__ = CircleAvatar
            CircleAvatar.__init__(self, self.id, self.GetCanvas(), (self.GetX(), self.GetY()), SensorColour(self.GetBrush().GetColour()))
        cnv = self.GetCanvas()
        cnv.Refresh()


    @wxThreadSwitch
    def GetSensorGraph(self):
        """Returns graph that the SensorBase object belongs to"""
        self.CheckValid()
        return self.GetCanvas()

    @wxThreadSwitch
    def __nonzero__(self):
        """API: Returns true if this object is on the Graph"""
        return bool(self.GetCanvas())

    @wxThreadSwitch
    def CheckValid(self):
        """API: Raises DeletedException if has been deleted"""
        if not self:
            print "Self %s, Boolean %s -- Sensor avatar accessed after deletion" %(self,bool(self))
            # raise DeletedException
        pass

    def OnLeftClick(self, x, y, keys = 0, attachment = 0):
        """Handles selection of Avatars.
        Allows the user to select nodes within the graph.
        Used to display a plot...
        """
        #pdb.set_trace()

    #def OnEndDragLeft(self, x, y, keys = 0, attachment = 0):
    #    """Handles the dragging of nodes within the graph.
    #    
    #    Allows the user to move nodes within the graph pane, and updates the moved nodes coordinates"""
    #    super(SensorAvatar, self).OnEndDragLeft(x, y, keys, attachment) # base_
    #
    #    if not self.Selected():
    #        self.OnLeftClick(x, y, keys, attachment)

    def OnMovePost(self, dc, x, y, oldX, oldY, display):
        """Clips to limit of Graph so you don't lose the Avatar off the edge"""
        from operator import sub
        canvas = self.GetCanvas()
        size = canvas.GetSize() #canvas.maxWidth, canvas.maxHeight
        xy = (x, y)
        bb = self.GetBoundingBoxMax()
        extent = map(sub, size, bb)
        newXY = map(max, xy, bb)
        newXY = map(min, newXY, extent)
        xy = map(None, xy) # ensure xy and newXY of same type

        #pdb.set_trace()
        if xy != newXY:
            self.Move(dc, newXY[0], newXY[1], display)
            #canvas.ClearBackground()
            canvas.Refresh()
        else:
            super(SensorAvatar, self).OnMovePost(dc, x, y, oldX, oldY, display) # base_

    #@wxThreadSwitch
    #def Selected(self):
    #   return super(SensorAvatar, self).Selected()

    @wxThreadSwitch
    def GetAvatarColour(self):
        """API: Returns the Colour of the Avatar"""
        self.CheckValid()
        return SensorColor(self.GetBrush().GetColour())
    GetAvatarColor = GetAvatarColour

    @wxThreadSwitch
    def SetAvatarColour(self, colour):
        """API: Sets the Colour of the Avatar"""
        self.CheckValid()
        canvas = self.GetCanvas()
        colour = SensorColour._getColour(colour)
        self.SetBrush(wx.Brush(colour))
        self.SetPen(AvatarPen())
        canvas.Refresh()
    SetAvatarColor = SetAvatarColour

    @wxThreadSwitch
    def AvatarPosition(self):
        """API: Returns the (x,y) coordinates of the Avatar"""
        self.CheckValid()
        return (self.GetX(), self.GetY())

    @wxThreadSwitch
    def MoveAvatar(self, xy):
        """API: Moves the node to new (x,y) coordinates"""
        self.CheckValid()
        x, y = xy
        canvas = self.GetCanvas()
        dc = wx.ClientDC(canvas)
        canvas.PrepareDC(dc)
        self.Move(dc, x, y, True)
        canvas.Refresh()

    @wxThreadSwitch
    def GetArcFrom(self, fromAvatar):
        """API: Returns arc from fromAvatar to self
        (or None if no such arc exists)"""
        self.CheckValid()
        for line in self.GetLines():
            if fromAvatar == line.GetFrom() and self == line.GetTo():
                return line

    @wxThreadSwitch
    def GetArcTo(self, toAvatar):
        """API: Returns arc from self to toAvatar
        (or None if no such arc exists)"""
        self.CheckValid()
        for line in self.GetLines():
            if self == line.GetFrom() and toAvatar == line.GetTo():
                return line

    @wxThreadSwitch
    def ArcsFromHere(self):
        """API: A list of all the Arcs pointing from this Avatar"""
        self.CheckValid()
        return [line for line in self.GetLines() if line.GetFrom() == self]

    @wxThreadSwitch
    def ArcsToHere(self):
        """API: A list of all the Arcs pointing to the Avatar"""
        self.CheckValid()
        return [line for line in self.GetLines() if line.GetTo() == self]

    @wxThreadSwitch
    def Delete(self):
        """Removes a SensorBase object from the graph"""
        try:
            self.SetPen(None)
            self.SetBrush(None)
            self.SetFont(None)
        finally:
            super(SensorAvatar, self).Delete()

    @wxThreadSwitch
    def DeleteFromGraph(self):
        """API: Deletes the Avatar from the Graph"""
        if self:
            for line in self.ArcsFromHere() + self.ArcsToHere():
                line.DeleteFromGraph()
            canvas = self.GetCanvas()
            self.Delete()
            self.SetCanvas(None)
            canvas.Refresh()

    @wxThreadSwitch
    def AddArcTo(self, toAvatar, colour):
        """API: Adds an arc from self to toAvatar, of given Colour"""
        self.CheckValid()
        canvas = self.GetCanvas()
        arc = MsgAvatar(self, toAvatar, SensorColour._getColour(colour))
	canvas.Refresh()
        return arc

    
class CircleAvatar(SensorAvatar, ogl.CircleShape):
    def __init__(self, id, canvas, xy, colour,size = 25):
        """Initialise a sensorAvatar object.
        
        This creates a new sensor node object on the graph. called from the 
        L{Dingo.OGL.SensorGraph.AddSensorAvatar} method (self, xy, colour,size=10)

        @type canvas: Dingo.OGL.SensorGraph
        @param canvas: the sensorgraph the node belongs to
        @type xy: tuple
        @param xy: Coordinates of the node as a tuple (X,Y)
        @type colour: Dingo.OGL.SensorColor
        @param colour: The L{Dingo.OGL.SensorColor} of the node
        @type size: number
        @param size: The diameter of the node drawn on the graph
        """
        ogl.CircleShape.__init__(self, size)
        SensorAvatar.__init__(self, id)
	
        self.SetDraggable(True, True)
        self.SetCanvas(canvas)
        x, y = xy
        self.SetX(x)
        self.SetY(y)
        self.SetFixedSize(True, True)
        self.SetPen(AvatarPen())
        colour = SensorColour._getColor(colour)
        self.SetBrush(wx.Brush(colour))

	self.SetFormatMode(ogl.FORMAT_CENTRE_VERT)
	self.SetTextColour("black")
        self.SetFont(wx.Font(6, wx.SWISS, wx.BOLD, wx.NORMAL))
	self.AddText(id)

        canvas.AddShape(self)
        self.Show(True)
        canvas.Refresh()
        return


class SquareAvatar(SensorAvatar, ogl.RectangleShape):
    def __init__(self, id, canvas, xy, colour,size = 25):
        """Initialise a sensorAvatar object.
        
        This creates a new sensor node object on the graph. called from the 
        L{Dingo.OGL.SensorGraph.AddSensorAvatar} method (self, xy, colour,size=10)

        @type canvas: Dingo.OGL.SensorGraph
        @param canvas: the sensorgraph the node belongs to
        @type xy: tuple
        @param xy: Coordinates of the node as a tuple (X,Y)
        @type colour: Dingo.OGL.SensorColor
        @param colour: The L{Dingo.OGL.SensorColor} of the node
        @type size: number
        @param size: The diameter of the node drawn on the graph
        """
        ogl.RectangleShape.__init__(self,size,size)
        SensorAvatar.__init__(self, id)
        self.SetDraggable(True, True)
        self.SetCanvas(canvas)
        x, y = xy
        self.SetX(x)
        self.SetY(y)
        self.SetFixedSize(True, True)
        self.SetPen(AvatarPen())
        colour = SensorColour._getColor(colour)
        self.SetBrush(wx.Brush(colour))

	self.SetFormatMode(ogl.FORMAT_CENTRE_VERT)
	self.SetTextColour("black")
        self.SetFont(wx.Font(6, wx.SWISS, wx.BOLD, wx.NORMAL))
	self.AddText(id)

        canvas.AddShape(self)
        self.Show(True)
        canvas.Refresh()
             
class PolyAvatar(SensorAvatar, ogl.PolygonShape):
    def __init__(self, id, canvas, xy, colour,polyList = None):
        """Initialise a sensorAvatar object.
        
        This creates a new sensor node object on the graph. called from the 
        L{Dingo.OGL.SensorGraph.AddSensorAvatar} method (self, xy, colour,size=10)
        
        @type canvas: Dingo.OGL.SensorGraph
        @param canvas: the sensorgraph the node belongs to
        @type xy: tuple
        @param xy: Coordinates of the node as a tuple (X,Y)
        @type colour: Dingo.OGL.SensorColor
        @param colour: The L{Dingo.OGL.SensorColor} of the node
        @type size: number
        @param size: The diameter of the node drawn on the graph
        """
        ogl.PolygonShape.__init__(self)
        SensorAvatar.__init__(self, id)        
        if not polyList:
            the_points = [(-5,0),(0,10),(5,0)]

        self.Create(polyList)
        #self.MakeControlPoints()
        self.SetDraggable(True, True)
        self.SetCanvas(canvas)
        x, y = xy
        self.SetX(x)
        self.SetY(y)
        self.SetFixedSize(True, True)
        self.SetPen(AvatarPen())
        colour = SensorColour._getColor(colour)
        self.SetBrush(wx.Brush(colour))

	self.SetFormatMode(ogl.FORMAT_CENTRE_VERT)
	self.SetTextColour("black")
        self.SetFont(wx.Font(6, wx.SWISS, wx.BOLD, wx.NORMAL))
	self.AddText(id)

        canvas.AddShape(self)
        self.Show(True)
        canvas.Refresh()


class TriangleAvatar(PolyAvatar):
    def __init__(self, id, canvas, xy, colour, size = 25):
        """Initialise a sensorAvatar object.
        
        This creates a new sensor node object on the graph. called from the 
        L{Dingo.OGL.SensorGraph.AddSensorAvatar} method (self, xy, colour,size=10)
        
        @type canvas: Dingo.OGL.SensorGraph
        @param canvas: the sensorgraph the node belongs to
        @type xy: tuple
        @param xy: Coordinates of the node as a tuple (X,Y)
        @type colour: Dingo.OGL.SensorColor
        @param colour: The L{Dingo.OGL.SensorColor} of the node
        @type size: number
        @param size: The diameter of the node drawn on the graph
        """
        ogl.PolygonShape.__init__(self)
        SensorAvatar.__init__(self, id)        
        the_points = [(-(size/2),0),(0,size),((size/2),0)]
        self.Create(the_points)
        #self.MakeControlPoints()
        self.SetDraggable(True, True)
        self.SetCanvas(canvas)
        x, y = xy
        self.SetX(x)
        self.SetY(y)
        self.SetFixedSize(True, True)
        self.SetPen(AvatarPen())
        colour = SensorColour._getColor(colour)
        self.SetBrush(wx.Brush(colour))

	self.SetFormatMode(ogl.FORMAT_CENTRE_VERT)
	self.SetTextColour("black")
        self.SetFont(wx.Font(6, wx.SWISS, wx.BOLD, wx.NORMAL))
	self.AddText(id)

        canvas.AddShape(self)
        self.Show(True)
        canvas.Refresh()


class MsgAvatar(ogl.LineShape):
    """API: The class of Arcs on the Graph.
    
    Arcs are used to denote communication between nodes. drawing a
    directional connecting line between any nodes communicating in the
    simulation
    """
    @wxThreadSwitch
    def __init__(self, fromAvatar, toAvatar, colour, isdashed=False):
        """Initialise a sensor arc.
        
        Draws a arc between two nodes on the graph

        @type fromAvatar: Dingo.OGL.SensorAvatar
        @param fromAvatar: The Avatar the arc comes from
        @type toAvatar: Dingo.OGL.SensorAvatar        
        @param toAvatar: To Avatar the arc finishes at
        @type colour: Dingo.OGL.SensorColour
        @param colour: A sensor colour object denoting the colour of the graph
        """
        ogl.LineShape.__init__(self)

        self.AddArrow(ogl.ARROW_ARROW, ogl.ARROW_POSITION_MIDDLE)
        self.MakeLineControlPoints(2)

        if fromAvatar is None and toAvatar is None: return # for arc pool
        
        canvas = fromAvatar.GetSensorGraph()
        self.SetCanvas(canvas)
        canvas.AddShape(self)

        fromX = fromAvatar.GetX()
        fromY = fromAvatar.GetY()
        toX = toAvatar.GetX()
        toY = toAvatar.GetY()
        self.SetEnds(fromX,fromY,toX,toY)
        self.SetArcColour(colour, isdashed)
        #fromAvatar.AddLine(self, toAvatar)
        self.Show(True)

    @wxThreadSwitch
    def drawOnCanvas(self, canvas, fromX, fromY, toX, toY, colour, isdashed=False):
        """Needed when using arc pool."""
        self.SetCanvas(canvas)
	self.SetArcColour(colour, isdashed)
        self.SetEnds(fromX,fromY,toX,toY)
        canvas.AddShape(self)
        #fromAvatar.AddLine(self, toAvatar)
        self.Show(True)
        canvas.Refresh()
        return
        
                     
	"""
	    stTm = time.clock()
        ogl.LineShape.__init__(self)
        canvas = fromAvatar.GetSensorGraph()
        self.SetCanvas(canvas)
	    fnCvs = time.clock()
        canvas.AddShape(self)
        self.SetPen(wx.Pen(colour))
        self.SetBrush(wx.Brush(colour))
        self.AddArrow(ogl.ARROW_ARROW, ogl.ARROW_POSITION_MIDDLE)
	    penTm = time.clock()
        self.MakeLineControlPoints(2)
        fromAvatar.AddLine(self, toAvatar)
        self.Show(True)
	    dcTm = time.clock()
        #dc = wx.ClientDC(canvas)
        #canvas.PrepareDC(dc)
        #self.Draw(dc)

	    enTm = time.clock()
	    print "Time Arc %f" %(enTm-stTm)
	"""
    @wxThreadSwitch
    def GetFromAvatar(self):
        """API: The Avatar from which this Arc points"""
        self.CheckValid()
        return self.GetFrom()

    @wxThreadSwitch
    def GetToAvatar(self):
        """API: The Avatar to which this Arc points"""
        self.CheckValid()
        return self.GetTo()

    @wxThreadSwitch
    def GetAvatars(self):
        """API: A (from, to) pair of Avatars and the ends of this Arc"""
        self.CheckValid()
        return (self.GetFrom(), self.GetTo())

    @wxThreadSwitch
    def GetArcColour(self):
        """API: Returns the Colour of this Arc"""
        self.CheckValid()
        return SensorColour(self.GetBrush().GetColour())
    GetArcColor = GetArcColour

    @wxThreadSwitch
    def SetArcColour(self, colour, isdashed=False):
        """API: Sets the Colour of this Arc"""
        self.CheckValid()#
        colour = SensorColour._getColour(colour)
        canvas = self.GetCanvas()
        dc = wx.ClientDC(canvas)
        canvas.PrepareDC(dc)

        pen = wx.Pen(colour)
        brush = wx.Brush(colour)
        if isdashed: pen.SetStyle(wx.SHORT_DASH)
        else: pen.SetStyle(wx.SOLID)

	self.SetPen(pen)
        self.SetBrush(brush)

        self.Draw(dc)
    SetArcColor = SetArcColour

    @wxThreadSwitch
    def DeleteFromGraph(self):
        """Deletes this Arc from the Graph"""
        if self:            	
            #fromShape = self.GetFrom()
            #if self in fromShape.GetLines():
            #    fromShape.RemoveLine(self)
            #toShape = self.GetTo()
            #if self in toShape.GetLines():
            #    toShape.RemoveLine(self)
            self.GetPen().SetColour(None)
	    self.SetPen(None)
            self.GetBrush().SetColour(None)
            self.SetBrush(None)
            self.SetEnds(0,0,0,0)
            canvas = self.GetCanvas()
            self.Delete()
            self.SetCanvas(None)
            canvas.Refresh()
	
    @wxThreadSwitch
    def CheckValid(self):
        """API: Raises DeletedException if has been deleted"""
        if not self:
            print "Self %s, Boolean %s -- Sensor avatar accessed after deletion" %(self,bool(self))
            # raise DeletedException
        pass


class AvatarPen(wx.Pen, object):
    __instance = None       
    def __new__(cls, *args, **kargs): 
        """Create a new instance of this class, ensuring conformance
        to the singleton pattern.

        See U{http://en.wikipedia.org/wiki/Singleton_pattern#Python}
        """
        if cls.__instance is None:
            cls.__instance = object.__new__(cls, *args, **kargs)
        return cls.__instance
    def __init__(self):
        wx.Pen.__init__(self, wx.NamedColour("black"), 2)
    	return
    

class BcastAvatarPen(wx.Pen, object):
    __instance = None       
    def __new__(cls, *args, **kargs): 
        """Create a new instance of this class, ensuring conformance
        to the singleton pattern.

        See U{http://en.wikipedia.org/wiki/Singleton_pattern#Python}
        """
        if cls.__instance is None:
            cls.__instance = object.__new__(cls, *args, **kargs)
        return cls.__instance
    def __init__(self):
        wx.Pen.__init__(self, wx.NamedColour("green"), 2)
    

class SensorColour(object):
    """API: The class of Colours of Avatars and Arcs.
    
    This is the base class that describes the colouring used within
    the L{Dingo.OGL.SensorGraph}.  This class contains methods for
    getting and setting the colour attributes of a node, and details
    of predefined colour settings to used within simulations
    """
    def __init__(self, colour):
        """Initialise a colour object.
        Creates a new colour object,  Returns a error if a invalid colour is specified
        
        @param colour: The colour to create"""
        object.__init__(self)
        if not isinstance(colour, wx.Color):
            raise TypeError('Not a wx.Color: ' + `colour`);
        self._colour = colour

    def _getColour(self):
        """Return the colour value of the object"""
        return self._colour
    _getColor = _getColour

    @wxThreadSwitch
    def GetRGBCombined(self):
        """API: returns the RGB value packed in a 3-byte integer"""
        return self._colour.GetRGB()

    @wxThreadSwitch
    def GetRGB(self):
        """API: returns a triple of (red, green, blue) intensities"""
        return self.GetRed(), self.GetGreen(), self.GetBlue()

    @wxThreadSwitch
    def GetRed(self):
        """API: Returns the red intensity"""
        return self._colour.Red()

    @wxThreadSwitch
    def GetGreen(self):
        """API: Returns the green intensity"""
        return self._colour.Green()

    @wxThreadSwitch
    def GetBlue(self):
        """API: Returns the blue intensity"""
        return self._colour.Blue()

    @wxThreadSwitch
    def GetName(self):
        """Returns the name of the Colour, if it has one,
        otherwise the empty string"""
        return wx.TheColourDatabase.FindName(self._colour)

    @wxThreadSwitch
    def __cmp__(self, other):

        """API: Used for defining ordering and equality semantics for Colours

        Compares the colour object to another using the
        L{Dingo.OGL.GetRGBCombined} value of each as comparator.
        
        @param other: The colour we wish to compare
        """
        if isinstance(other, SensorColour):
            return cmp(self.GetRGBCombined(), other.GetRGBCombined())
        return NotImplemented

    @wxThreadSwitch
    def __hash__(self):
        """API: Hashing consistent with __cmp__"""
        return self.GetRGBCombined()

    @wxThreadSwitch
    @staticmethod
    def FromName(name):
        """API: Creates a new Colour with given name, or raises ValueError
        The colour to be created should be a WX colour object
        @param name:  The WX.colour name of the colour to create"""
        c = wx.NamedColour(name)
        if c:
            return SensorColour(c)
        else:
            raise ValueError('Invalid colour name: ' + name)

    @wxThreadSwitch
    @staticmethod
    def ExistsNamed(name):
        """API: Returns whether or not exists Colour with given name
        
        @param name: The name we wish to check exists"""
        return bool(wx.TheColourDatabase.FindColour(name))

    @wxThreadSwitch
    @staticmethod
    def FromRGBCombined(rgb):
        """API: Unpacks (red, green, blue) from 3-byte int to make Colour"""
        return SensorColour(wx.ColorRGB(rgb))

    @wxThreadSwitch
    @staticmethod
    def FromRGB(r, g, b):
        """API: Creates colour from a R,G,B values
        
        @param r:  Red value of created colour
        @param g:  Green value of created colour
        @param b:  Blue value of created colour """
        return SensorColour(wx.Color(r, g, b))

    @wxThreadSwitch
    @staticmethod
    def _setupColours():
        """Setup a list of colours we can create by name.
        
        This initialises a selection of possible sensor colours to use, allowing us to call commonly used colours by a 
        identifier instead of creating and testing each from scratch.
        """
        SensorColour.Aquamarine = SensorColour.FromName('Aquamarine')
        SensorColour.Black = SensorColour.FromName('Black')
        SensorColour.Blue = SensorColour.FromName('Blue')
        SensorColour.BlueViolet = SensorColour.FromName('Blue Violet')
        SensorColour.Brown = SensorColour.FromName('Brown')
        SensorColour.CadetBlue = SensorColour.FromName('Cadet Blue')
        SensorColour.Coral = SensorColour.FromName('Coral')
        SensorColour.CornflowerBlue = SensorColour.FromName('Cornflower Blue')
        SensorColour.Cyan = SensorColour.FromName('Cyan')
        SensorColour.DarkGrey = SensorColour.FromName('Dark Grey')
        SensorColour.DarkGreen = SensorColour.FromName('Dark Green')
        SensorColour.DarkOliveGreen = SensorColour.FromName('Dark Olive Green')
        SensorColour.DarkOrchid = SensorColour.FromName('Dark Orchid')
        SensorColour.DarkSlateBlue = SensorColour.FromName('Dark Slate Blue')
        SensorColour.DarkSlateGrey = SensorColour.FromName('Dark Slate Grey')
        SensorColour.DarkTurquoise = SensorColour.FromName('Dark Turquoise')
        SensorColour.DimGrey = SensorColour.FromName('Dim Grey')
        SensorColour.Firebrick = SensorColour.FromName('Firebrick')
        SensorColour.ForestGreen = SensorColour.FromName('Forest Green')
        SensorColour.Gold = SensorColour.FromName('Gold')
        SensorColour.Goldenrod = SensorColour.FromName('Goldenrod')
        SensorColour.Grey = SensorColour.FromName('Grey')
        SensorColour.Green = SensorColour.FromName('Green')
        SensorColour.GreenYellow = SensorColour.FromName('Green Yellow')
        SensorColour.IndianRed = SensorColour.FromName('Indian Red')
        SensorColour.Khaki = SensorColour.FromName('Khaki')
        SensorColour.LightBlue = SensorColour.FromName('Light Blue')
        SensorColour.LightGrey = SensorColour.FromName('Light Grey')
        SensorColour.LightSteelBlue = SensorColour.FromName('Light Steel Blue')
        SensorColour.LimeGreen = SensorColour.FromName('Lime Green')
        SensorColour.Magenta = SensorColour.FromName('Magenta')
        SensorColour.Maroon = SensorColour.FromName('Maroon')
        SensorColour.MediumAquamarine = SensorColour.FromName('Medium Aquamarine')
        SensorColour.MediumBlue = SensorColour.FromName('Medium Blue')
        SensorColour.MediumForestGreen = SensorColour.FromName('Medium Forest Green')
        SensorColour.MediumGoldenrod = SensorColour.FromName('Medium Goldenrod')
        SensorColour.MediumOrchid = SensorColour.FromName('Medium Orchid')
        SensorColour.MediumSeaGreen = SensorColour.FromName('Medium Sea Green')
        SensorColour.MediumSlateBlue = SensorColour.FromName('Medium Slate Blue')
        SensorColour.MediumSpringGreen = SensorColour.FromName('Medium Spring Green')
        SensorColour.MediumTurquoise = SensorColour.FromName('Medium Turquoise')
        SensorColour.MediumVioletRed = SensorColour.FromName('Medium Violet Red')
        SensorColour.MidnightBlue = SensorColour.FromName('Midnight Blue')
        SensorColour.Navy = SensorColour.FromName('Navy')
        SensorColour.Orange = SensorColour.FromName('Orange')
        SensorColour.OrangeRed = SensorColour.FromName('Orange Red')
        SensorColour.Orchid = SensorColour.FromName('Orchid')
        SensorColour.PaleGreen = SensorColour.FromName('Pale Green')
        SensorColour.Pink = SensorColour.FromName('Pink')
        SensorColour.Plum = SensorColour.FromName('Plum')
        SensorColour.Purple = SensorColour.FromName('Purple')
        SensorColour.Red = SensorColour.FromName('Red')
        SensorColour.Salmon = SensorColour.FromName('Salmon')
        SensorColour.SeaGreen = SensorColour.FromName('Sea Green')
        SensorColour.Sienna = SensorColour.FromName('Sienna')
        SensorColour.SkyBlue = SensorColour.FromName('Sky Blue')
        SensorColour.SlateBlue = SensorColour.FromName('Slate Blue')
        SensorColour.SpringGreen = SensorColour.FromName('Spring Green')
        SensorColour.SteelBlue = SensorColour.FromName('Steel Blue')
        SensorColour.Tan = SensorColour.FromName('Tan')
        SensorColour.Thistle = SensorColour.FromName('Thistle')
        SensorColour.Turquoise = SensorColour.FromName('Turquoise')
        SensorColour.Violet = SensorColour.FromName('Violet')
        SensorColour.VioletRed = SensorColour.FromName('Violet Red')
        SensorColour.Wheat = SensorColour.FromName('Wheat')
        SensorColour.White = SensorColour.FromName('White')
        SensorColour.Yellow = SensorColour.FromName('Yellow')
        SensorColour.YellowGreen = SensorColour.FromName('Yellow Green')
SensorColor = SensorColour
